package org.mbds.wolf.android.omapi;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;

import org.mbds.wolf.android.tools.FileUtilities;
import org.mbds.wolf.exception.ByteArrayToHexaStringException;
import org.mbds.wolf.seql.APDUModel;
import org.mbds.wolf.seql.AppletModel;
import org.mbds.wolf.seql.ISeqlCallBack;
import org.mbds.wolf.seql.ISeqlController;
import org.mbds.wolf.seql.SeqlMetadata;
import org.mbds.wolf.seql.SeqlParser;
import org.mbds.wolf.seql.SeqlWords;
import org.mbds.wolf.seql.exceptions.APDUError;
import org.mbds.wolf.seql.exceptions.BadRequestException;
import org.mbds.wolf.seql.exceptions.NoReaderException;
import org.mbds.wolf.seql.exceptions.NoSecureElementException;
import org.mbds.wolf.tools.BytesTool;
import org.simalliance.openmobileapi.Channel;
import org.simalliance.openmobileapi.Reader;
import org.simalliance.openmobileapi.SEService;
import org.simalliance.openmobileapi.Session;

import android.content.Context;

/**
 * 
 * @author alesas
 *
 */
public class SeqlOmapiController implements ISeqlController, SEService.CallBack 
{
	public final static String DEFAULT_CONFIG_FILE_NAME = "config.xml";
	protected SEService seService;
	private File configFile;
	private Context context;
	private ISeqlCallBack callback;
	private Channel channel;
	private AppletModel currentApplet;

	/**
	 * Constructor SeqlOmapiController
	 * @param context
	 * @param callback
	 * @param File XML config file
	 * @throws IOException 
	 */
	public SeqlOmapiController(Object context, ISeqlCallBack callback, File config) throws IOException 
	{
		setCallback(callback);
		this.context = (Context) context;
		setConfigFile(config);
		initService();
	}
	/**
	 * Constructor SeqlOmapiController
	 * @param context
	 * @param callback
	 * @throws IOException 
	 */
	public SeqlOmapiController(Object context, ISeqlCallBack callback) throws IOException 
	{
		this(context, callback, FileUtilities.getFileFromAssets((Context)context, DEFAULT_CONFIG_FILE_NAME));
	}
	

	/**
	 * sayHello (SELECT AID APDU ISO 7816-4)
	 * @param appletName
	 * @param commandId
	 * @return boolean
	 */
	@Override
	public boolean sayHello(String appletName, int commandHelloId) 
	{
		Map<String, Object> response = new HashMap<String, Object>();
		try 
		{
			AppletModel applet = null;
			try 
			{
				applet = SeqlMetadata.getInstance(configFile).getAppletByAlias(appletName);
			} 
			catch (Exception e) 
			{
				e.printStackTrace();
			}
			if (applet==null)
			{
				callback.onNoApplet();
			}

			openChannel(applet);

			byte[] hello = channel.getSelectResponse();

			if (APDUModel.isResponseSucceded(hello)) 
			{
				response.put("hello", "Applet is succesfully selected!");
				callback.onResponse(response, commandHelloId);
				return true;
			}
			else 
			{
				callback.onNoApplet();
			}
		} 
		catch (NoSuchElementException e) 
		{

			callback.onNoApplet();

		} 
		catch (IOException e) 
		{

			callback.onNoSecureElement();

		} 
		catch (NoReaderException e) 
		{

			callback.onNoReader();

		} 
		catch (NoSecureElementException e) 
		{

			callback.onNoSecureElement();

		} 
		catch (BadRequestException e) 
		{
			callback.onBadRequest(e.getMessage());
		}
		return false;
	}

	/**
	 * initService
	 * @return boolean
	 */
	@Override
	public boolean initService() 
	{
		if (seService == null || !isServiceConnected())
		{
			seService = new SEService(context, this);
		}
		return true;
	}

	/**
	 * disconnect
	 */
	public void disconnect() 
	{
		try 
		{
			if (channel != null)
			{
				channel.getSession().close();
			}
			if (seService != null && seService.isConnected()) 
			{
				seService.shutdown();
			}
		} 
		catch (Throwable t) 
		{
			t.printStackTrace();
		}
	}


	/**
	 * serviceConnected
	 * @param SEService
	 */
	@Override
	public void serviceConnected(SEService service) 
	{
		if (service.isConnected())
		{
			callback.onConnected();
		}
		else
		{
			callback.onNotConnected();
		}
	}

	/**
	 * getCallback
	 * @return ISeqlCallBack
	 */
	public ISeqlCallBack getCallback() 
	{
		return callback;
	}

	/**
	 * setCallback
	 * @param ISeqlCallBack
	 */
	@Override
	public void setCallback(ISeqlCallBack callback) 
	{
		this.callback = callback;
	}

	/**
	 * execute
	 * @param command
	 * @param commandId
	 * @return boolean
	 */
	@Override
	public boolean execute(final String command, final int commandId) 
	{
		//new Thread(){
			//public void run(){
		Map<String, Object> maps = null;
		try {

	        maps = SeqlParser.parseSeql(command, configFile);

			if (maps == null)
				throw new BadRequestException("Command not supported!");
			return executeCommand(maps, commandId);

		} catch (BadRequestException ex) {
			ex.printStackTrace();
			callback.onBadRequest(ex.getMessage());
		} catch (NoSuchElementException ex) {
			ex.printStackTrace();
			callback.onNoApplet();
		} catch (NoSecureElementException ex) {
			ex.printStackTrace();
			callback.onNoSecureElement();
		} catch (NoReaderException ex) {
			ex.printStackTrace();
			callback.onNoReader();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (Exception e) {
			callback.onBadRequest(e.getMessage());
		}
			//}
		//}.start();
		return false;
	}


	/**
	 * executeCommand
	 * @param map of APDU commands (Structure: Applet<-Instructions<-Fields<-Commands)
	 * @param commandId
	 * @return boolean
	 * @throws BadRequestException
	 * @throws NoSecureElementException
	 * @throws NoSuchElementException
	 * @throws NoReaderException
	 * @throws IOException
	 */
	boolean executeCommand(Map<String, Object> maps, int commandId) throws 	BadRequestException, 
																			NoSecureElementException,
																			NoSuchElementException, 
																			NoReaderException, 
																			IOException 
	{
		Map<String, Object> response = new HashMap<String, Object>();
		AppletModel applet = null;
		Object a = maps.get(SeqlWords.STR_APPLET);

		if (a instanceof AppletModel) 
		{
			applet = (AppletModel) a;
		}
		
		if (applet == null) 
		{
			callback.onNoApplet();
			return false;
		}
		
		boolean success = false;
		
		channel = openChannel(applet);
		
		byte[][] commands = (byte[][]) maps.get(SeqlWords.STR_COMMAND);
		String[] fields = (String[]) maps.get(SeqlWords.STR_FIELD);
		
		int idxField = 0;
		for (byte[] command : commands) 
		{
			byte[] resp = sendCommand(command);
			
			try 
			{
				System.out.println("Response: "+BytesTool.byteArrayToHexString(resp));
			} 
			catch (ByteArrayToHexaStringException e) 
			{
				//
			}
			if (!APDUModel.isResponseSucceded(resp)) 
			{
				if (APDUModel.isPinRequired(resp)) 
				{
					callback.onPINRequired();
				} 
				else 
				{
					APDUError error = new APDUError("Wrong APDU response", APDUModel.getResponseStatusWord(resp));
					callback.onAPDUError(error);
				}
			} 
			else 
			{
				String r = APDUModel.getResponseBody(resp);
				response.put(fields[idxField], r);
				// Repeat fields loop for each instruction sequence
				idxField = (idxField == fields.length? 0: idxField + 1);
				success = true;
			}

			if (!success) 
			{
				//TODO: rollback all transactions!!!
				break;
			}
		}
		if (success) 
		{
			//System.out.println("Response sending...........");
			callback.onResponse(response, commandId);
		}
		return success;
	}

	/**
	 * sendCommand
	 * @param command
	 * @return byte[] response
	 * @throws IOException
	 */
	public byte[] sendCommand(byte[] command) throws IOException 
	{
		return channel.transmit(command);
	}

	/**
	 * isServiceConnected
	 * @return boolean
	 */
	@Override
	public boolean isServiceConnected() 
	{
		return (seService != null) && seService.isConnected();
	}

	/**
	 * 
	 * @return
	 * @throws NoReaderException
	 * @throws NoSecureElementException
	 */
	private Reader getPrincipalReader() throws 	NoReaderException,
												NoSecureElementException 
	{

		Reader reader = null;
		Reader[] readers = seService.getReaders();

		if (readers.length == 0)
		{
			throw new NoReaderException();
		}

		for (Reader r : readers) 
		{

			System.out.println(r.getName());
			reader = r;

			if (r.isSecureElementPresent()) 
			{
				return reader;
			}
		}
		// No SE present
		throw new NoSecureElementException();
	}


	/**
	 * openSession
	 * @return Session
	 * @throws NoReaderException
	 * @throws IOException
	 * @throws NoSecureElementException
	 */
	@SuppressWarnings("unused")
	private Session openSession() throws 	NoReaderException, 
											IOException, 
											NoSecureElementException 
	{
		Session session = getPrincipalReader().openSession();
		return session;
	}

	/**
	 * openChannel
	 * @param applet
	 * @return Channel
	 * @throws IOException
	 * @throws NoSuchElementException
	 * @throws NoReaderException
	 * @throws NoSecureElementException
	 * @throws BadRequestException
	 */
	private Channel openChannel(AppletModel applet) throws 	IOException, 
															NoSuchElementException, 
															NoReaderException, 
															NoSecureElementException, 
															BadRequestException 
	{
		// Check if a channel is opened
		if (channel != null && !channel.isClosed()) 
		{
			if (currentApplet != null && (applet.getAlias().equalsIgnoreCase(currentApplet.getAlias()))) 
			{
				// use already opened channel for this applet
				return channel;
			}
			else
			{
				// close  opened channel and open a new one for this applet
				channel.close();
			}
		}

		Reader reader = getPrincipalReader();

		if (reader != null) 
		{
			Session session = getPrincipalReader().openSession();
			byte[] aid;
			try 
			{
				aid = BytesTool.hexStringToByteArray(applet.getAID());
				System.out.println(applet.getAID());
				channel = session.openLogicalChannel(aid);
				
				if (APDUModel.isResponseSucceded(channel.getSelectResponse()))
				{
					// set current selected applet
					currentApplet = applet;
				}
				
				return channel;
			} 
			catch (Exception e) 
			{
				throw new BadRequestException(e.getMessage());
			}

		}
		throw new NoSuchElementException("No secure element present");
	}

	public File getConfigFile() {
		return configFile;
	}

	@Override
	public void setConfigFile(File configFile) {
		this.configFile = configFile;
	}
}